[AWS CDK] CloudFront Functionでレスポンスにセキュリティヘッダーを追加する
こんにちは、CX事業本部 IoT事業部の若槻です。
今までCloudFront + S3という構成でクライアントへのレスポンスヘッダーをカスタマイズしたい場合はLambda@Edgeを使う場合が多かったですが、最近リリースされた、よりユーザーに近いロケーションでより高速な処理が可能なCloudFront Functionsでも実装が可能です。
今回は、このCloudFront Functionを使用してレスポンスにセキュリティヘッダーを追加する設定をAWS CDKで実装してみました。
CloudFront Functionコード
function handler(event) { var response = event.response; var headers = response.headers; // Set HTTP security headers // Since JavaScript doesn't allow for hyphens in variable names, we use the dict["key"] notation headers['strict-transport-security'] = { value: 'max-age=63072000; includeSubdomains; preload'}; //headers['content-security-policy'] = { value: "default-src 'none'; img-src 'self'; script-src 'self'; style-src 'self'; object-src 'none'"}; headers['x-content-type-options'] = { value: 'nosniff'}; headers['x-frame-options'] = {value: 'DENY'}; headers['x-xss-protection'] = {value: '1; mode=block'}; // Return the response to viewers return response; }
コードはAWS公式のサンプルを参考にしました。Lambda@Edgeとは記法が異なることに注意してください。今回利用したWebコンテンツ(React)だとcontent-security-policy
ヘッダーは制限が厳しかったため元のコードから削除しました。
CDKコード
CloudFront + S3 + CloudFront Function から成る静的ホスティングなWebサイトです。
import * as cdk from "@aws-cdk/core"; import * as cloudfront from "@aws-cdk/aws-cloudfront"; import * as s3 from "@aws-cdk/aws-s3"; import * as s3deploy from "@aws-cdk/aws-s3-deployment"; import * as iam from "@aws-cdk/aws-iam"; export class SampleAppStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); const accountId = cdk.Stack.of(this).account; const websiteBucket = new s3.Bucket(this, "WebsiteBucket", { bucketName: `sample-app-${accountId}`, websiteErrorDocument: "index.html", websiteIndexDocument: "index.html", }); const websiteIdentity = new cloudfront.OriginAccessIdentity( this, "WebsiteIdentity", { comment: `sample-app-identity`, } ); const webSiteBucketPolicyStatement = new iam.PolicyStatement({ actions: ["s3:GetObject"], effect: iam.Effect.ALLOW, principals: [ new iam.CanonicalUserPrincipal( websiteIdentity.cloudFrontOriginAccessIdentityS3CanonicalUserId ), ], resources: [`${websiteBucket.bucketArn}/*`], }); websiteBucket.addToResourcePolicy(webSiteBucketPolicyStatement); // CloudFront Functionリソースの定義 const addHeaderFunction = new cloudfront.Function( this, "AddHeaderFunction", { functionName: `add-header`, code: cloudfront.FunctionCode.fromFile({ filePath: "lambda/add-header/index.js", }), } ); const websiteDistribution = new cloudfront.CloudFrontWebDistribution( this, "WebsiteDistribution", { comment: `website-distribution`, errorConfigurations: [ { errorCachingMinTtl: 300, errorCode: 403, responseCode: 200, responsePagePath: "/index.html", }, { errorCachingMinTtl: 300, errorCode: 404, responseCode: 200, responsePagePath: "/index.html", }, ], originConfigs: [ { s3OriginSource: { s3BucketSource: websiteBucket, originAccessIdentity: websiteIdentity, }, behaviors: [ { isDefaultBehavior: true, // CloudFront FunctionをDistributionに設定 functionAssociations: [ { eventType: cloudfront.FunctionEventType.VIEWER_RESPONSE, function: addHeaderFunction, }, ], }, ], }, ], priceClass: cloudfront.PriceClass.PRICE_CLASS_ALL, } ); new s3deploy.BucketDeployment(this, "WebsiteDeploy", { sources: [s3deploy.Source.asset("./web/build")], destinationBucket: websiteBucket, distribution: websiteDistribution, distributionPaths: ["/*"], }); } }
DistributionへのFunctionの紐付けは、Lambda@Edgeの場合はlambdaFunctionAssociations
を使用しますが、CloudFront Functionの場合はfunctionAssociations
を使用することに注意してください。またヘッダー追加はビューワーへのレスポンス時に行うため、eventType
の指定はVIEWER_RESPONSE
とします。
cdk deploy
によりスタックをデプロイします。
AWSコンソールでのFunctionのテスト
CloudFront > Functionsで作成したFunctionを選択します。
アップロードされたコードが確認できます。[Test]タブをクリックします。
[Test]をクリックしてテストを実行してみます。
するとエラーとなりました。
ログを見るとheaders
プロパティを取得できなかったからのようです。
Error Message: The CloudFront function associated with the CloudFront distribution is invalid or could not run. TypeError: cannot get property "headers" of undefined
次は[Sample test events]でViewer response with headers
を指定し、[Test]をクリックして実行します。
今度はテストが成功しました。
実際にブラウザからアクセス
Webサイトにアクセスしてブラウザのデバッグツールからレスポンスヘッダーを見ると、セキュリティヘッダーが追加されていることが確認できました。
注意点
Lambda@Edge(L@E)と異なりCloudFront Function(CF2)のレスポンスはキャッシュされないため、今まで通りL@Eを使用した方が低コストな場合もあるようです。以下の岩田の記事にとてもよくまとめられています。
理由としては、L@Eならオリジンレスポンスでのヘッダー追加がキャッシュされるため初回アクセスでの実行のみで済むところが、CF2ならビューワーレスポンスによりアクセス毎に必ず実行されることにされるためです。 CloudFront Functions の導入 – 任意の規模において低レイテンシーでコードをエッジで実行 | Amazon Web Services ブログより引用
ただし岩田も記事末尾で述べている通り、キャッシュの有効期限やキャッシュヒット率、ブラウザキャッシュなども考慮する必要があり、どちらが適しているかは配信するウェブサイトの特性によっても変わってきます。またFunctionの実行時間はCF2は最大1ミリ秒のため、体感的なレイテンシーはほぼ無さそうです。L@EとCF2両者の特性を踏まえた上で適切なサービスを選択するようにしましょう。
おわりに
CloudFront Functionを使用してレスポンスにセキュリティヘッダーを追加する設定をAWS CDKで実装してみました。
今までLambda@Edge Functionで行っていた処理をCloudFront Functionに寄せることが出来るようになることで、ユーザー観点だと処理の高速化に期待が持てますし、また開発者観点だとフロントエンドのCDKのコードにLambdaのパッケージを含める必要が無くなり、CloudFrontに一本化できるようになるのが嬉しいところです。
参考
以上